package com.tencent.qqpay.sdk;

import java.util.HashMap;
import java.util.Map;

import com.tencent.qqpay.sdk.QQPayConstants.SignType;

/**
 * QQ支付API接口封装类。
 * @see com.github.qqpay.sdk.WXPay
 */
public class QQPay {
    private QQPayConfig config; // QQ支付配置
    private SignType signType; // 签名算法
    private String notifyUrl; // 支付结果通知地址
    private QQPayRequest qqPayRequest; // QQ支付请求封装类

    /**
     * QQ支付API接口封装类。
     * @param config QQ支付配置
     * @throws Exception
     */
    public QQPay(final QQPayConfig config) throws Exception {
        this(config, null);
    }
    
    /**
     * QQ支付API接口封装类。
     * @param config QQ支付配置
     * @param notifyUrl 通知
     * @throws Exception
     */
    public QQPay(final QQPayConfig config, final String notifyUrl) throws Exception {
        this.config = config;
        this.notifyUrl = notifyUrl;
        this.signType = SignType.MD5;
        this.qqPayRequest = new QQPayRequest(config);
    }

    /**
     * 检查QQ支付配置。
     * @throws Exception
     */
    private void checkQQPayConfig() throws Exception {
        if (this.config == null) {
            throw new Exception("config is null");
        }
        if (this.config.getAppID() == null || this.config.getAppID().trim().length() == 0) {
            throw new Exception("appid in config is empty");
        }
        if (this.config.getMchID() == null || this.config.getMchID().trim().length() == 0) {
            throw new Exception("appid in config is empty");
        }
        if (this.config.getCertStream() == null) {
            throw new Exception("cert stream in config is empty");
        }
        if (this.config.getHttpConnectTimeoutMs() < 10) {
            throw new Exception("http connect timeout is too small");
        }
        if (this.config.getHttpReadTimeoutMs() < 10) {
            throw new Exception("http read timeout is too small");
        }

    }

    /**
     * 向 Map中添加 appid、mch_id、sub_mch_id、nonce_str、sign_type、sign <br>
     * 该函数适用于商户适用于统一下单等接口，不适用于红包、代金券接口.
     *
     * @param reqData 向QQ支付 post的请求数据
     * @return API返回数据
     * @throws Exception
     */
    public Map<String, String> fillRequestData(Map<String, String> reqData) throws Exception {
        reqData.put("appid", config.getAppID());
        reqData.put("mch_id", config.getMchID());
        reqData.put("nonce_str", QQPayUtil.generateNonceStr());
        if (SignType.MD5.equals(this.signType)) {
            reqData.put("sign_type", QQPayConstants.MD5);
        }
        reqData.put("sign", QQPayUtil.generateSignature(reqData, config.getKey(), this.signType));
        return reqData;
    }

    /**
     * 判断xml数据的sign是否有效，必须包含sign字段，否则返回false。
     *
     * @param reqData 向qqpay post的请求数据
     * @return 签名是否有效
     * @throws Exception
     */
    public boolean isResponseSignatureValid(Map<String, String> reqData) throws Exception {
        // 返回数据的签名方式和请求中给定的签名方式是一致的
        return QQPayUtil.isSignatureValid(reqData, this.config.getKey(), this.signType);
    }

    /**
     * 判断支付结果通知中的sign是否有效。
     *
     * @param reqData 向QQ支付 post的请求数据
     * @return 签名是否有效
     * @throws Exception
     */
    public boolean isPayResultNotifySignatureValid(Map<String, String> reqData) throws Exception {
        String signTypeInData = reqData.get(QQPayConstants.FIELD_SIGN_TYPE);
        SignType signType;
        if (signTypeInData == null) {
            signType = SignType.MD5;
        } else {
            signTypeInData = signTypeInData.trim();
            if (signTypeInData.length() == 0) {
                signType = SignType.MD5;
            } else if (QQPayConstants.MD5.equals(signTypeInData)) {
                signType = SignType.MD5;
            } else {
                throw new Exception(String.format("Unsupported sign_type: %s", signTypeInData));
            }
        }
        return QQPayUtil.isSignatureValid(reqData, this.config.getKey(), signType);
    }

    /**
     * 不需要证书的请求。
     * @param urlSuffix 接口地址后缀
     * @param reqData 向QQ支付 post的请求数据
     * @param connectTimeoutMs 超时时间，单位是毫秒
     * @param readTimeoutMs 超时时间，单位是毫秒
     * @return API返回数据
     * @throws Exception
     */
    public String requestWithoutCert(String urlSuffix, Map<String, String> reqData,
                                     int connectTimeoutMs, int readTimeoutMs) throws Exception {
        String msgUUID = reqData.get("nonce_str");
        String reqBody = QQPayUtil.mapToXml(reqData);

        return this.qqPayRequest.requestWithoutCert(urlSuffix, msgUUID, reqBody, connectTimeoutMs, readTimeoutMs);
    }

    /**
     * 需要证书的请求。
     * @param urlSuffix 接口地址后缀
     * @param reqData 向QQ支付 post的请求数据  Map
     * @param connectTimeoutMs 超时时间，单位是毫秒
     * @param readTimeoutMs 超时时间，单位是毫秒
     * @return API返回数据
     * @throws Exception
     */
    public String requestWithCert(String urlSuffix, Map<String, String> reqData,
                                  int connectTimeoutMs, int readTimeoutMs) throws Exception {
        String msgUUID= reqData.get("nonce_str");
        String reqBody = QQPayUtil.mapToXml(reqData);

        return this.qqPayRequest.requestWithCert(urlSuffix, msgUUID, reqBody, connectTimeoutMs, readTimeoutMs);
    }

    /**
     * 处理 HTTPS API返回数据，转换成Map对象。return_code为SUCCESS时，验证签名。
     * @param xmlStr API返回的XML格式数据
     * @return Map类型数据
     * @throws Exception
     */
    public Map<String, String> processResponseXml(String xmlStr) throws Exception {
        String RETURN_CODE = "return_code";
        String return_code;
        Map<String, String> respData = QQPayUtil.xmlToMap(xmlStr);
        if (respData.containsKey(RETURN_CODE)) {
            return_code = respData.get(RETURN_CODE);
        } else {
            throw new Exception(String.format("No `return_code` in XML: %s", xmlStr));
        }

        if (QQPayConstants.FAIL.equals(return_code)) {
            return respData;
        } else if (QQPayConstants.SUCCESS.equals(return_code)) {
           if (this.isResponseSignatureValid(respData)) {
               return respData;
           } else {
               throw new Exception(String.format("Invalid sign value in XML: %s", xmlStr));
           }
        } else {
            throw new Exception(String.format("return_code value %s is invalid in XML: %s", return_code, xmlStr));
        }
    }

    /**
     * 作用：统一下单。<br>
     * 场景：除被扫支付场景以外，商户系统先调用该接口在QQ钱包服务后台生成预支付交易单，返回正确的预支付交易回话标识后再按具体的场景生成交易串调起支付。
     * @param reqData 向QQ支付 post的请求数据
     * @return API返回数据
     * @throws Exception
     */
    public Map<String, String> unifiedOrder(Map<String, String> reqData) throws Exception {
        return this.unifiedOrder(reqData, config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs());
    }


    /**
     * 作用：统一下单。<br>
     * 场景：除被扫支付场景以外，商户系统先调用该接口在QQ钱包服务后台生成预支付交易单，返回正确的预支付交易回话标识后再按具体的场景生成交易串调起支付。
     * @param reqData 向QQ支付 post的请求数据
     * @param connectTimeoutMs 连接超时时间，单位是毫秒
     * @param readTimeoutMs 读超时时间，单位是毫秒
     * @return API返回数据
     * @throws Exception
     */
    public Map<String, String> unifiedOrder(Map<String, String> reqData,  int connectTimeoutMs, int readTimeoutMs) throws Exception {
        String url = QQPayConstants.UNIFIEDORDER_URL_SUFFIX;
        if(this.notifyUrl != null) {
            reqData.put("notify_url", this.notifyUrl);
        }
        String respXml = this.requestWithoutCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs);
        return this.processResponseXml(respXml);
    }


    /**
     * 作用：查询订单。<br>
     * 场景：该接口提供所有手Q钱包订单的查询，商户可以通过该接口主动查询订单状态，完成下一步的业务逻辑。<br>
     * 需要调用查询接口的情况：<br>
     * 当商户后台、网络、服务器等出现异常，商户系统最终未接收到支付通知；<br>
     * 调用支付接口后，返回系统错误或未知交易状态情况；<br>
     * 调用 qpay_close_order.cgi 关闭订单接口之前，需确认支付状态；<br>
     * @param reqData 向QQ支付 post的请求数据
     * @return API返回数据
     * @throws Exception
     */
    public Map<String, String> orderQuery(Map<String, String> reqData) throws Exception {
        return this.orderQuery(reqData, config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs());
    }


    /**
     * 作用：查询订单。<br>
     * 场景：该接口提供所有手Q钱包订单的查询，商户可以通过该接口主动查询订单状态，完成下一步的业务逻辑。<br>
     * 需要调用查询接口的情况：<br>
     * 当商户后台、网络、服务器等出现异常，商户系统最终未接收到支付通知；<br>
     * 调用支付接口后，返回系统错误或未知交易状态情况；<br>
     * 调用 qpay_close_order.cgi 关闭订单接口之前，需确认支付状态；<br>
     * @param reqData 向QQ支付 post的请求数据
     * @param connectTimeoutMs 连接超时时间，单位是毫秒
     * @param readTimeoutMs 读超时时间，单位是毫秒
     * @return API返回数据
     * @throws Exception
     */
    public Map<String, String> orderQuery(Map<String, String> reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception {
        String url = QQPayConstants.ORDERQUERY_URL_SUFFIX;
        String respXml = this.requestWithoutCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs);
        return this.processResponseXml(respXml);
    }

    /**
     * 作用：关闭订单。<br>
     * 场景：<br>
     * 1、商户订单支付失败需要生成新单号重新发起支付，要对原订单号调用关单，避免重复支付；<br>
     * 2、系统下单后，用户支付超时，系统退出不再受理，避免用户继续，请调用关单接口;
     * @param reqData 向qqpay post的请求数据
     * @return API返回数据
     * @throws Exception
     */
    public Map<String, String> closeOrder(Map<String, String> reqData) throws Exception {
        return this.closeOrder(reqData, config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs());
    }

    /**
     * 作用：关闭订单。<br>
     * 场景：<br>
     * 1、商户订单支付失败需要生成新单号重新发起支付，要对原订单号调用关单，避免重复支付；<br>
     * 2、系统下单后，用户支付超时，系统退出不再受理，避免用户继续，请调用关单接口;
     * @param reqData 向QQ支付 post的请求数据
     * @param connectTimeoutMs 连接超时时间，单位是毫秒
     * @param readTimeoutMs 读超时时间，单位是毫秒
     * @return API返回数据
     * @throws Exception
     */
    public Map<String, String> closeOrder(Map<String, String> reqData,  int connectTimeoutMs, int readTimeoutMs) throws Exception {
        String url = QQPayConstants.CLOSEORDER_URL_SUFFIX;
        String respXml = this.requestWithoutCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs);
        return this.processResponseXml(respXml);
    }


    /**
     * 作用：申请退款。<br>
     * 场景：当交易发生之后一段时间内，由于买家或者卖家的原因需要退款时，卖家可以通过退款接口将支付款退还给买家，QQ钱包将在收到退款请求并且验证成功之后，按照退款规则将支付款按原路退到买家帐号上。
     * 其他：需要证书
     * @param reqData 向QQ支付 post的请求数据
     * @return API返回数据
     * @throws Exception
     */
    public Map<String, String> refund(Map<String, String> reqData) throws Exception {
        return this.refund(reqData, this.config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs());
    }


    /**
     * 作用：申请退款。<br>
     * 场景：当交易发生之后一段时间内，由于买家或者卖家的原因需要退款时，卖家可以通过退款接口将支付款退还给买家，QQ钱包将在收到退款请求并且验证成功之后，按照退款规则将支付款按原路退到买家帐号上。<br>
     * 其他：需要证书
     * @param reqData 向QQ支付 post的请求数据
     * @param connectTimeoutMs 连接超时时间，单位是毫秒
     * @param readTimeoutMs 读超时时间，单位是毫秒
     * @return API返回数据
     * @throws Exception
     */
    public Map<String, String> refund(Map<String, String> reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception {
        String url = QQPayConstants.REFUND_URL_SUFFIX;
        String respXml = this.requestWithCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs);
        return this.processResponseXml(respXml);
    }


    /**
     * 作用：退款查询。<br>
     * 场景：提交退款申请后，通过调用该接口查询退款状态。退款有一定延时，用余额支付的退款20分钟内到账，银行卡支付的退款3个工作日后重新查询退款状态。
     * @param reqData 向QQ支付 post的请求数据
     * @return API返回数据
     * @throws Exception
     */
    public Map<String, String> refundQuery(Map<String, String> reqData) throws Exception {
        return this.refundQuery(reqData, this.config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs());
    }


    /**
     * 作用：退款查询。<br>
     * 场景：提交退款申请后，通过调用该接口查询退款状态。退款有一定延时，用余额支付的退款20分钟内到账，银行卡支付的退款3个工作日后重新查询退款状态。
     * @param reqData 向QQ支付 post的请求数据
     * @param connectTimeoutMs 连接超时时间，单位是毫秒
     * @param readTimeoutMs 读超时时间，单位是毫秒
     * @return API返回数据
     * @throws Exception
     */
    public Map<String, String> refundQuery(Map<String, String> reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception {
        String url = QQPayConstants.REFUNDQUERY_URL_SUFFIX;
        String respXml = this.requestWithoutCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs);
        return this.processResponseXml(respXml);
    }


    /**
     * 作用：对账单下载（成功时返回对账单数据，失败时返回XML格式数据）。<br>
     * 场景：刷卡支付、公共号支付、扫码支付、APP支付
     * @param reqData 向QQ支付 post的请求数据
     * @return API返回数据
     * @throws Exception
     */
    public Map<String, String> downloadBill(Map<String, String> reqData) throws Exception {
        return this.downloadBill(reqData, this.config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs());
    }


    /**
     * 作用：对账单下载。<br>
     * 场景：商户可以通过该接口下载历史交易清单。<br>
     * @param reqData 向QQ支付 post的请求数据
     * @param connectTimeoutMs 连接超时时间，单位是毫秒
     * @param readTimeoutMs 读超时时间，单位是毫秒
     * @return 经过封装的API返回数据
     * @throws Exception
     */
    public Map<String, String> downloadBill(Map<String, String> reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception {
        String url = QQPayConstants.DOWNLOADBILL_URL_SUFFIX;
        String respStr = this.requestWithoutCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs).trim();
        Map<String, String> ret;
        // 出现错误，返回XML数据
        if (respStr.indexOf('<') == 0) {
            ret = QQPayUtil.xmlToMap(respStr);
        } else {
            // 正常返回csv数据
            ret = new HashMap<>();
            ret.put("return_code", QQPayConstants.SUCCESS);
            ret.put("return_msg", "ok");
            ret.put("data", respStr);
        }
        return ret;
    }

}
